#!/usr/bin/perl -w
# Configure the network interfaces
# vim:tw=100 sw=2 expandtab ft=perl
#
# This is considered a dangerous operation and will always be run in safe mode
# unless "-f network" is specified
#
# Example:
# interfaces => {
#   eth0 => {
#     primary => 1,
#     ip => '192.168.1.5',
#     netmask => '255.255.255.0',
#     gateway => '192.168.1.1',
#     mac => '00:50:8d:6d:c6:0c', # If not specified, the OS will choose which int is eth0
#     routes => {
#       '10.0.0.1/24' => '192.168.1.1', # Route via a gateway
#       '10.0.0.0/24' => undef,         # Route directly out this interface
#     },
#   },
#   'eth0:1' => {
#     family => 'inet6',
#     method => 'static',
#     ip => '2002:389:d010:7d00:251:9dff:fe6d:c60c',
#     netmask => 64,
#     gateway => '2002:389:d010:7d00',
#   },
#   eth1 => {
#     disabled => 1, # This interface will be added but not enabled
#     mac => '00:50:8d:6d:c6:0d',
#   },
#   ppp0 => {
#     method => 'ppp',
#     provider => 'dsl-provider', # A filename in /etc/ppp/peers
#   },
#   eth2 =>{
#     dhcp => 1,
#     mac => '00:50:8d:6d:c6:0e',
#     hostname => 'test-client',
#   },
#   # Bonding interface created from eth0 & eth1 which should not already be configured
#   bond0 => {
#     ip => '192.168.1.5',
#     netmask => '255.255.255.0',
#     gateway => '192.168.1.1',
#     bond => [ 'eth0', 'eth1' ],
#     multicast => 'no',
#     stp => 'off',
#   },
# },
# resolver => {   # Optional
#   timeout => 5,
#   attempts => 2,
# },
#
# Available skip_steps items:
#  interface - Configure any interfaces
#  bond - Configure bonding interfaces
#  bridge - Configure bridging interfaces
#  routes - Add static routes
#  resolvconf - Add name servers and resolv options
#  hostname - Set local hostname
#  domain_name - Set local domain name
#  iftab - Add mac address <-> interface name mapping

local $safe_mode = grep(/^network$/, @force_dangerous) ? 0 : 1;

my $changes = 0;

my %ints = flatten_hash(c("$hostname/interfaces"));

if (i_should("interface") && -d "/etc/sysconfig/network-scripts") {
  # RedHat style
  my %interface_files;
  while (my($device, $d) = each %ints) {
    next if $d->{disabled};
    my %c = (
      device => $device,
      type => "Ethernet",
      ipv6init => "". ($d->{ipv6} ? "yes" : "no"),
      userctl => "no",
      onboot => "". ($d->{inactive} ? "no" : "yes"),
    );
    foreach (qw( gateway bootproto type stp arp multicast userctl netmask bridge )) {
      $c{$_} = $d->{$_} if defined $d->{$_};
    }
    $c{hwaddr} = $d->{mac} if defined $d->{mac} && i_should("iftab");

    if ($d->{ip}) {
      $c{ipaddr} = $d->{ip};
      $c{netmask} ||= "255.255.255.0";
    } elsif ($d->{dhcp}) {
      $c{bootproto} = "dhcp";
      $c{dhcp_hostname} = ($d->{dhcp} !~ /^1$/ ? $d->{dhcp} : $hostname),
      $c{peerdns} = "yes";
    }
    $interface_files{$device} ||= \%c;

    if (i_should("bond")) {
      if ($device =~ /^bond(\d+)/) {
        foreach (@{$d->{slaves} || $d->{bond} || []}) {
          # Create a device entry for each slave interface
          my %c = (
            device => $_,
            onboot => "no",
            type => "Ethernet",
            master => $device,
            slave => "yes",
            peerdns => "yes",
            userctl => "no",
            ipv6init => "no",
          );
          $c{hwaddr} = $m{$hostname}->{interfaces}{$_}{mac}
            if i_should("iftab") && $m{$hostname}->{interfaces}{$_}{mac};
          $interface_files{$_} = \%c;
        }
      }
    }

    if (i_should("routes")) {
      my $routes = join("", map { "$_ via $d->{routes}->{$_} dev $device\n" }
        keys %{$d->{routes} || {}});
      if ($routes && text_install("/etc/sysconfig/network-scripts/route-$device", $routes, undef,
          { mode => 0644 })) {
        $changes++;
        if (!$safe_mode) {
          w "Network configuration has been changed for $device, you must restart the interface ".
            "manually to apply the changes.";
        }
      }
    }
  }

  my $alerted = 0;
  if (keys %interface_files) {
    if (i_should("bond")) {
      if (grep /^bond\d+$/, keys %interface_files) {
        file_append("/etc/modprobe.conf",
                    "options bonding mode=1 miimon=100 downdelay=300 updelay=300 max_bonds=4",
                    qr/^options bond(?:\d+|ing) /);
      }
    }
    while (my($int, $c) = each %interface_files) {
      file_append("/etc/modprobe.conf", "alias $int bonding", qr/^alias $int/)
        if i_should("bond") && $int =~ /^bond\d+$/;

      if (text_install("/etc/sysconfig/network-scripts/ifcfg-$int",
                       join("", map { uc($_). "=$c->{$_}\n" } sort keys %$c), undef, { mode => 0644 })) {
        $changes++;
        if (!$safe_mode) {
          w "Network configuration has been changed for $int, you must restart the interface ".
            "manually to apply the changes.";
        }
      }
    }
  }
} elsif (i_should("interface") && -f "/etc/network/interfaces") {
  # Debian style
  my %interfaces = (
    lo => {
      _auto => 1,
      _addrfam => 'inet',
      _method => 'loopback',
    },
  );
  my %macs;
  while (my($device, $d) = each %ints) {
    my %x = (
      _auto => $d->{disabled} ? 0 : 1,
      _addrfam => $d->{family} || 'inet',
      _method => $d->{dhcp} ? 'dhcp' : ($d->{method} || "static"),
      _mac => $d->{mac},
      address => $d->{ip},
      netmask => $d->{netmask} || ($d->{ip} ? "255.255.255.0" : undef),
      'pre-up' => [],
      up => [],
      'post-up' => [],
      'pre-down' => [],
      down => [],
      'post-down' => [],
    );
    # This is a list of all the options that can be specified, according to interfaces(5)
    $x{$_} = $d->{$_}
      foreach qw( bootfile broadcast client gateway hostname hwaddress leasehours leasetime media
                  metric mtu network pointopoint provider server vendor frame netnum endpoint local
                  gateway ttl );
    push @{$x{$_}}, ref $d->{$_} ? @{$d->{$_}} : $d->{$_}
      foreach qw( pre-up up post-up pre-down down post-down );

    if (i_should("bridge") && $d->{bridge}) {
      # Bridging Interface
      $m{$hostname}->{packages} ||= [];
      push @{$m{$hostname}->{packages}}, "bridge-utils";
      $x{bridge_ports} = join(" ", @{$d->{bridge}});
      $x{bridge_fd} = $d->{bridge_fd} || 9;
      $x{bridge_hello} = $d->{bridge_hello} || 2;
      $x{bridge_maxage} = $d->{bridge_maxage} || 12;
      $x{bridge_stp} = $d->{bridge_stp} || "off";
    }

    if (i_should("routes")) {
      while (my($dest, $gw) = each(%{$d->{routes} || {}})) {
        if ($dest =~ /\//) {
          $dest = "-net $dest";
        } else {
          $dest = "-host $dest";
        }
        if ($gw) {
          push @{$x{up}}, "route add $dest gw $gw";
        } else {
          push @{$x{up}}, "route add $dest dev $device";
        }
      }
    }
    $interfaces{$device} = \%x;
  }

  if (keys %interfaces) {
    my $text = "# This file describes the network interfaces available on your system\n".
               "# and how to activate them. For more information, see interfaces(5).\n";
    while (my($name, $int) = each %interfaces) {
      next if $int->{_disabled};
      $text .= "\n";
      $text .= "auto $name\n" if $int->{_auto};
      $text .= "iface $name $int->{_addrfam} $int->{_method}\n";
      foreach my $k (grep { defined $int->{$_} } sort keys %$int) {
        next if $k =~ /^_/;
        if (ref $int->{$k} eq 'ARRAY') {
          $text .= "\t$k $_\n" foreach grep { defined $_ } @{$int->{$k}};
        } else {
          $text .= "\t$k $int->{$k}\n" unless ref $int->{$k};
        }
      }

      if ($int->{_mac} && i_should("iftab")) {
        if (-f "/etc/udev/rules.d/70-persistent-net.rules") {
          file_append(-file => "/etc/udev/rules.d/70-persistent-net.rules",
                      -add => "SUBSYSTEM==\"net\", ACTION==\"add\", DRIVERS==\"?*\", ".
                              "ATTR{address}==\"$int->{_mac}\", ATTR{dev_id}==\"0x0\", ".
                              "ATTR{type}==\"1\", KERNEL==\"eth*\", NAME=\"$name\"",
                      -match => qr/NAME=['"]$name['"]/) && $changes++;
        } elsif (-f "/etc/iftab") {
          file_append(-file => "/etc/iftab",
                      -add => "$name mac $int->{_mac} arp 1",
                      -match => qr/^$name/) && $changes++;
        }
      }
    }

    if (text_install("/etc/network/interfaces", $text)) {
      $changes++;
      if (!$safe_mode) {
        w "Network configuration has been changed, you must restart the interfaces manually to ".
          "apply the changes.";
      }
    }
  }
}

if (i_should("hostname")) {
  if (hostname ne $hostname) {
    l "Changing hostname from ". hostname. " to $hostname";
    command("hostname", $hostname) && $changes++;
    if (-f "/etc/hostname") {
      text_install(-file => "/etc/hostname", -text => $hostname, -uid => 0, -mode => 0644)
        && $changes++
    } elsif (-f "/etc/sysconfig/network") {
      file_modify(-file => "/etc/sysconfig/network",
                  -modify => [ "s/^HOSTNAME=.*/HOSTNAME=$hostname/" ]) && $changes++
    }
  }
}

if (i_should("resolvconf")) {
  file_append(-file => "/etc/resolv.conf",
              -add => "options timeout:". c("$hostname/resolver/timeout", 5).
                      " attempts:". c("$hostname/resolver/attempts", 2),
              -match => qr/^options/) && $changes++;
  file_append(-file => "/etc/resolv.conf", -add => "nameserver $_", -match => qr/^nameserver $_/)
    && $changes++ foreach uniq(flatten_list(c("$hostname/nameservers")));
}

if (i_should("domain_name")) {
  foreach (c("$hostname/domain_name")) {
    file_append("/etc/resolv.conf", "search $_", qr/^search /);
    last;
  }
}

if ($safe_mode && $changes) {
  return l "Network config is considered dangerous, changes won't be applied \nunless you specify ".
           "the \"-f network\" argument.";
}

